Example PRO

import {
  Button, HStack, Map, MapPolyline, Marker, Navigation, NavigationStack, Picker,
  Script, ScrollView, Text, useEffect, useObservable, useState, VStack,
} from "scripting"

// Shanghai landmarks used across the demos.
const PEOPLES_SQUARE = { latitude: 31.2304, longitude: 121.4737 }
const BUND = { latitude: 31.2407, longitude: 121.4905 }
const LUJIAZUI = { latitude: 31.2397, longitude: 121.4994 }

const initialRegion = {
  center: { latitude: 31.2354, longitude: 121.4905 },
  span: { latitudeDelta: 0.04, longitudeDelta: 0.04 },
}

// Phase 3e: distance / duration formatting delegates to MapUtils, which wraps
// MKDistanceFormatter / DateComponentsFormatter so the output respects the
// device locale.
const fmtDistance = (meters: number) => MapUtils.formatDistance(meters)
const fmtDuration = (seconds: number) => MapUtils.formatDuration(seconds)

// ───────────────────────────────────────────────────────────────────────
// Demo 1: calculate a single route and render the polyline
// ───────────────────────────────────────────────────────────────────────
function SingleRouteDemo() {
  // Phase 3g: 拿到完整 response 而不是单挑 route — 这样 source / destination 的
  // MapItem 可以直接喂给 <Marker item={...}>,触发 Apple 的 auto POI glyph。
  const [resp, setResp] = useState<MapDirections.DirectionsResponse | null>(null)
  const [loading, setLoading] = useState(false)
  const [err, setErr] = useState<string | null>(null)
  const position = useObservable<MapCameraPosition>(MapCameraPosition.region(initialRegion))

  const route = resp?.routes[0] ?? null

  const calc = async () => {
    setLoading(true); setErr(null)
    try {
      const r = await MapDirections.calculate({
        source: { coordinate: PEOPLES_SQUARE, name: "People's Square" },
        destination: { coordinate: LUJIAZUI, name: "Lujiazui" },
        transportType: "automobile",
      })
      setResp(r)
      const fit = MapUtils.regionFromCoordinates(r.routes[0].coordinates, 0.2)
      if (fit) position.setValue(MapCameraPosition.region(fit))
    } catch (e) {
      setErr(String(e))
    } finally {
      setLoading(false)
    }
  }

  return <VStack alignment={"leading"} spacing={8}>
    <Text font={"headline"}>1. Calculate + render</Text>
    <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
      {`Drive from People's Square to Lujiazui. The returned \`route.coordinates\` polyline
      feeds straight into \`<MapPolyline coordinates={...}>\`; the response's
      \`source\` / \`destination\` MapItems are dropped into \`<Marker item={...}>\`
      so MapKit picks the POI glyph automatically.`}
    </Text>
    <HStack spacing={8}>
      <Button title={loading ? "Calculating..." : "Calculate"} action={calc} />
      {route != null
        ? <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
          {fmtDistance(route.distance)} · {fmtDuration(route.expectedTravelTime)} · {route.steps.length} steps
        </Text>
        : null}
    </HStack>
    {err != null
      ? <Text font={"caption"} foregroundStyle={"systemRed"}>{err}</Text>
      : null}
    <Map
      cameraPosition={position}
      frame={{ height: 280 }}
      clipShape={{ type: "rect", cornerRadius: 12 }}
    >
      {resp != null
        ? <>
          {/* MapItem 形态:title / coordinate / glyph 都由 MapKit 自己挑。 */}
          <Marker item={resp.source} tint="systemGreen" />
          <Marker item={resp.destination} tint="systemRed" />
        </>
        : <>
          {/* 路线还没算出来时回落到坐标形态。 */}
          <Marker title="Start" coordinate={PEOPLES_SQUARE} tint="systemGreen" />
          <Marker title="End" coordinate={LUJIAZUI} tint="systemRed" />
        </>}
      {route != null
        ? <MapPolyline
          coordinates={route.coordinates}
          strokeColor="systemBlue"
          strokeStyle={{ lineWidth: 4, lineCap: "round", lineJoin: "round" }}
        />
        : null}
    </Map>
  </VStack>
}

// ───────────────────────────────────────────────────────────────────────
// Demo 2: switch transportType and watch the route change shape / length
// ───────────────────────────────────────────────────────────────────────
function TransportTypeDemo() {
  type Mode = "automobile" | "walking"
  const [mode, setMode] = useState<Mode>("walking")
  const [resp, setResp] = useState<MapDirections.DirectionsResponse | null>(null)
  const [loading, setLoading] = useState(false)
  const [err, setErr] = useState<string | null>(null)
  const position = useObservable<MapCameraPosition>(MapCameraPosition.region(initialRegion))

  const route = resp?.routes[0] ?? null

  useEffect(() => {
    let cancelled = false
    setLoading(true); setErr(null)
    MapDirections.calculate({
      source: { coordinate: BUND, name: "Bund" },
      destination: { coordinate: LUJIAZUI, name: "Lujiazui" },
      transportType: mode,
    }).then(r => {
      if (cancelled) return
      setResp(r)
      const fit = MapUtils.regionFromCoordinates(r.routes[0].coordinates, 0.2)
      if (fit) position.setValue(MapCameraPosition.region(fit))
    }).catch(e => {
      if (cancelled) return
      setErr(String(e))
    }).finally(() => {
      if (!cancelled) setLoading(false)
    })
    return () => { cancelled = true }
  }, [mode])

  return <VStack alignment={"leading"} spacing={8}>
    <Text font={"headline"}>2. transportType switch</Text>
    <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
      Same start/end (Bund → Lujiazui) but different transport mode. Driving prefers
      bridges + roads; walking takes a much more direct path.
    </Text>
    <Picker
      title="Mode"
      value={mode}
      onChanged={(v: any) => setMode(v as Mode)}
      pickerStyle={"segmented"}
    >
      <Text tag={"automobile"}>automobile</Text>
      <Text tag={"walking"}>walking</Text>
    </Picker>
    {loading ? <Text font={"caption2"}>Calculating...</Text> : null}
    {err != null ? <Text font={"caption"} foregroundStyle={"systemRed"}>{err}</Text> : null}
    {route != null
      ? <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
        {route.transportType} · {fmtDistance(route.distance)} · {fmtDuration(route.expectedTravelTime)}
      </Text>
      : null}
    <Map
      cameraPosition={position}
      frame={{ height: 260 }}
      clipShape={{ type: "rect", cornerRadius: 12 }}
    >
      {resp != null
        ? <>
          <Marker item={resp.source} tint="systemRed" />
          <Marker item={resp.destination} tint="systemPurple" />
        </>
        : <>
          <Marker title="Bund" coordinate={BUND} tint="systemRed" />
          <Marker title="Lujiazui" coordinate={LUJIAZUI} tint="systemPurple" />
        </>}
      {route != null
        ? <MapPolyline
          coordinates={route.coordinates}
          strokeColor={mode === "walking" ? "systemTeal" : "systemBlue"}
          strokeStyle={{ lineWidth: 4, lineCap: "round" }}
        />
        : null}
    </Map>
  </VStack>
}

// ───────────────────────────────────────────────────────────────────────
// Demo 3: alternates — render multiple routes side by side
// ───────────────────────────────────────────────────────────────────────
const ALTERNATE_COLORS = ["systemBlue", "systemOrange", "systemPurple"] as const

function AlternatesDemo() {
  const [resp, setResp] = useState<MapDirections.DirectionsResponse | null>(null)
  // 选中的 route 在地图上 stroke 加粗 + 不透明,未选中的回落到细 + 半透明,
  // 视觉上把"导航选了哪条"立刻表达清楚。
  const [selectedIdx, setSelectedIdx] = useState(0)
  const [loading, setLoading] = useState(false)
  const [err, setErr] = useState<string | null>(null)
  const position = useObservable<MapCameraPosition>(MapCameraPosition.region(initialRegion))

  const routes = resp?.routes ?? []

  const calc = async () => {
    setLoading(true); setErr(null)
    try {
      const r = await MapDirections.calculate({
        source: { coordinate: PEOPLES_SQUARE, name: "People's Square" },
        destination: { coordinate: LUJIAZUI, name: "Lujiazui" },
        transportType: "automobile",
        requestsAlternateRoutes: true,
      })
      setResp(r)
      setSelectedIdx(0)
      const allCoords = r.routes.flatMap(rt => rt.coordinates)
      const fit = MapUtils.regionFromCoordinates(allCoords, 0.2)
      if (fit) position.setValue(MapCameraPosition.region(fit))
    } catch (e) {
      setErr(String(e))
    } finally {
      setLoading(false)
    }
  }

  return <VStack alignment={"leading"} spacing={8}>
    <Text font={"headline"}>3. Alternates + selection</Text>
    <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
      {`\`requestsAlternateRoutes: true\` may return up to 3 driving routes. Tap a row
      below to highlight that route — the selected polyline renders thicker and
      fully opaque, others fade to make the choice obvious.`}
    </Text>
    <HStack spacing={8}>
      <Button title={loading ? "Calculating..." : "Calculate (with alternates)"} action={calc} />
    </HStack>
    {err != null ? <Text font={"caption"} foregroundStyle={"systemRed"}>{err}</Text> : null}
    <VStack alignment={"leading"} spacing={4}>
      {routes.map((r, i) => {
        const isSelected = i === selectedIdx
        const color = ALTERNATE_COLORS[i % ALTERNATE_COLORS.length]
        return <Button
          key={i}
          action={() => setSelectedIdx(i)}
          buttonStyle={isSelected ? "borderedProminent" : "bordered"}
          tint={color}
        >
          <HStack spacing={8}>
            <Text font={"caption"} foregroundStyle={isSelected ? "white" : color}>
              {`${i + 1}. ${r.name || "(unnamed)"}`}
            </Text>
            <Text font={"caption2"} foregroundStyle={isSelected ? "white" : "secondaryLabel"}>
              {`${fmtDistance(r.distance)} · ${fmtDuration(r.expectedTravelTime)}`}
            </Text>
          </HStack>
        </Button>
      })}
    </VStack>
    <Map
      cameraPosition={position}
      frame={{ height: 280 }}
      clipShape={{ type: "rect", cornerRadius: 12 }}
    >
      {resp != null
        ? <>
          <Marker item={resp.source} tint="systemGreen" />
          <Marker item={resp.destination} tint="systemRed" />
        </>
        : <>
          <Marker title="Start" coordinate={PEOPLES_SQUARE} tint="systemGreen" />
          <Marker title="End" coordinate={LUJIAZUI} tint="systemRed" />
        </>}
      {/* 未选中的先画(细),选中的最后画压在上面(粗)。MapPolyline 目前不接收
          opacity / 视图层 modifier,这里用 stroke width(3 vs 6)制造主次对比。 */}
      {routes.map((r, i) => {
        if (i === selectedIdx) return null
        return <MapPolyline
          key={`alt-${i}`}
          coordinates={r.coordinates}
          strokeColor={ALTERNATE_COLORS[i % ALTERNATE_COLORS.length]}
          strokeStyle={{ lineWidth: 3, lineCap: "round" }}
        />
      })}
      {routes[selectedIdx] != null
        ? <MapPolyline
          key={`sel-${selectedIdx}`}
          coordinates={routes[selectedIdx].coordinates}
          strokeColor={ALTERNATE_COLORS[selectedIdx % ALTERNATE_COLORS.length]}
          strokeStyle={{ lineWidth: 6, lineCap: "round", lineJoin: "round" }}
        />
        : null}
    </Map>
  </VStack>
}

// ───────────────────────────────────────────────────────────────────────
// Demo 4: calculateETA — time / distance only, no geometry download
// ───────────────────────────────────────────────────────────────────────
function ETADemo() {
  type Mode = "automobile" | "walking"
  const [mode, setMode] = useState<Mode>("automobile")
  const [eta, setEta] = useState<MapDirections.ETAResponse | null>(null)
  const [loading, setLoading] = useState(false)
  const [err, setErr] = useState<string | null>(null)

  const calc = async () => {
    setLoading(true); setErr(null)
    try {
      const result = await MapDirections.calculateETA({
        source: { coordinate: PEOPLES_SQUARE, name: "People's Square" },
        destination: { coordinate: LUJIAZUI, name: "Lujiazui" },
        transportType: mode,
        departureDate: new Date(),
      })
      setEta(result)
    } catch (e) {
      setErr(String(e))
    } finally {
      setLoading(false)
    }
  }

  return <VStack alignment={"leading"} spacing={8}>
    <Text font={"headline"}>4. `calculateETA`</Text>
    <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
      Skip the polyline download; get back travel time / distance / arrival window only.
      Cheaper than `calculate` when the UI just needs a headline number.
    </Text>
    <Picker
      title="Mode"
      value={mode}
      onChanged={(v: any) => setMode(v as Mode)}
      pickerStyle={"segmented"}
    >
      <Text tag={"automobile"}>automobile</Text>
      <Text tag={"walking"}>walking</Text>
    </Picker>
    <HStack spacing={8}>
      <Button title={loading ? "Calculating..." : "Calculate ETA"} action={calc} />
    </HStack>
    {err != null ? <Text font={"caption"} foregroundStyle={"systemRed"}>{err}</Text> : null}
    {eta != null
      ? <VStack alignment={"leading"} spacing={2}>
        <Text font={"caption"}>{`Travel: ${fmtDuration(eta.expectedTravelTime)}`}</Text>
        <Text font={"caption"}>{`Distance: ${fmtDistance(eta.distance)}`}</Text>
        <Text font={"caption2"} foregroundStyle={"secondaryLabel"}>
          {`Depart: ${eta.expectedDepartureDate.toLocaleTimeString()}`}
        </Text>
        <Text font={"caption2"} foregroundStyle={"secondaryLabel"}>
          {`Arrive: ${eta.expectedArrivalDate.toLocaleTimeString()}`}
        </Text>
      </VStack>
      : null}
  </VStack>
}

function Example() {
  const dismiss = Navigation.useDismiss()

  return <NavigationStack>
    <ScrollView
      navigationTitle="Map Directions"
      toolbar={{
        cancellationAction: <Button
          title="Close"
          action={dismiss}
        />
      }}
    >
      <VStack
        navigationTitle={"MapDirections"}
        navigationBarTitleDisplayMode={"inline"}
        spacing={20}
        padding
      >
        <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
          {`\`MapDirections.calculate(...)\` returns route polylines that plug straight into
          \`<MapPolyline coordinates={...}>\`. \`MapDirections.calculateETA(...)\` returns
          time / distance only — handy when you don't need the geometry. No system
          permissions required.`}
        </Text>
        <SingleRouteDemo />
        <TransportTypeDemo />
        <AlternatesDemo />
        <ETADemo />
      </VStack>
    </ScrollView>
  </NavigationStack>
}

async function run() {
  await Navigation.present({ element: <Example /> })
  Script.exit()
}

run()